<?php


/*
 * Created on 2009-6-30
 * Author guobin@yizin.net
 */

class Chain
{
    private $extensions;

    private $keys = Array();

    private $current = Array();

    public function __construct($extensions)
    {

        $this->extensions = $extensions;
        $this->keys[0] = array_keys($this->extensions);
        $this->keys[1] = array_keys($this->extensions[$this->keys[0][0]]);
        $this->current[0] = 0;
        $this->current[1] = 0;
    }

    public function next( & $context)
    {

        if ($this->current[0] == count($this->keys[0]))
        {
            return;
        }

        $previous = Array(
        $this->keys[0][$this->current[0]],
        $this->keys[1][$this->current[1]]
        );

        $this->current[1]++;
        if ($this->current[1] == count($this->keys[1]))
        {

            $this->current[1] = 0;
            $this->current[0]++;

            if ($this->current[0] < count($this->keys[0]))
            {
                $this->keys[1] = array_keys($this->extensions[$this->keys[0][$this->current[0]]]);
            }
        }

        $this->extensions[$previous[0]][$previous[1]]->apply($context, $this);

    }
}

class Extendable
{
    public $id;
    public $introduction;

    public function __construct($id, $introduction = "")
    {
        $this->id = $id;
        $this->introduction = $introduction;
    }
}

abstract class Extension
{
    public $id;
    public $extendable;
    public $name;
    public $priority;
    public $introduction;
    public $feature;

    public function __construct($extendable, $name = "", $priority = 10, $introduction = false)
    {
        $this->extendable = $extendable;
        $this->name = $name;
        $this->priority = $priority;
        $this->introduction = $introduction;
    }

    /*
     * to be overrided in the child class.
     * Should contain codes like $chain->next($context)
     * to apply next extension appliable in the chain.
     * If a callback function name $appiable was passed
     * the $appiable will be called here.
     */
    public abstract function apply( & $context, Chain $chain);
}

class ExtensionManager
{
    private $extensions;

    public function getExtensions()
    {
        return $this->extensions;
    }

    public function register($id, Extension $extension)
    {
        $this->extensions[$id][$extension->priority][$extension->id] = $extension;
    }

    public function apply($id, & $context)
    {

        if ( isset ($this->extensions[$id]) && is_array($this->extensions[$id]))
        {
            ksort($this->extensions[$id]);
            $chain = new Chain($this->extensions[$id]);
            $chain->next($context);
        }
    }
}

abstract class Feature
{
    public $id;
    public $name;
		public $uniqType;
    public $introduction;
    public $extensions = Array();
    public $extendables = Array();
    public $status; //"ENABLED | SUSPENDED | DISABLED"

    public function __construct($name = "", $introduction = false, $uniqType = "")
    {
        $this->name = $name;
        $this->introduction = $introduction;
				$this->uniqType = $uniqType;

        $this->registerExtension();
        $this->registerExtendable();

        //specify feature attribute for every extension
        foreach ($this->extensions as $extension)
        {
            $extension->feature = $this;
        }
    }

    /*
     * to be overrided, registering extensions.
     */
    abstract public function registerExtension();

    /*
     * to be overrided, registering extensions.
     */

    abstract public function registerExtendable();

    /*
     * to be overrided, evoked when installing this feature.
     */
    abstract public function init();

    /*
     * to be overrided, evoked when uninstalling this feature.
     */
    abstract public function destroy();
}

class FeatureManager
{
    public $extensionManager;

    public $featurePath = Array();

    public $features = Array();

    public $runtimePath;
	
    public $preservedFeatures;

    private $featureStoreFile = "feature.store";

    private $featureStore = Array();

    private $extensionStoreFile = "extension.store";

    private $extensionStore = Array();


    public function __construct($featurePath, $runtimePath, $preservedFeatures = Array())
    {
        $this->runtimePath = $runtimePath;

        if (!file_exists($this->runtimePath))
        {
            mkdir($this->runtimePath, 0777, true);
        }

        $this->extensionManager = new ExtensionManager();

        if ($featurePath)
        {
            $featurePathArray = explode(PATH_SEPARATOR, $featurePath);
            foreach ($featurePathArray as $path)
            {
                if (is_dir($path))
                {
                    $this->featurePath[] = $this->trimrslash($path);
                }
            }
        }

        $this->preservedFeatures = $preservedFeatures;

        $this->importFeatureStore();
        $this->importExtensionStore();
    }

    public function load()
    {
        foreach ($this->featurePath as $path)
        {
            if ($dh = opendir($path))
            {
                while (($file = readdir($dh)) !== false)
                {
                    $id = basename($file);
                    if (strpos($id, ".") !== 0)
                    {
                        $this->resolve($path, $id);
                    }
                }
            }
            closedir($dh);
        }
    }

    protected function resolve($path, $id)
    {
        $featureClass = false;
        $featureName = "";
				$featureUniqType = "";
        $featureIntroduction = "";
        require_once ($path.DIRECTORY_SEPARATOR.$id.DIRECTORY_SEPARATOR."feature.php");

        if (! isset ($this->features[$featureClass]))
        {
            $feature = false;
            if ($featureClass && class_exists($featureClass))
            {
                $feature = new $featureClass($featureName, $featureIntroduction, $featureUniqType);
            }

            if ($feature)
            {
                $feature->id = $featureClass;

                $status = $this->featureStore[$feature->id];
                $feature->status = $status?$status:"DISABLED";

                $this->features[$featureClass] = $feature;

                if ($feature->status == "ENABLED")
                {
                    foreach ($feature->extensions as $extension)
                    {
                        $extensionClass = get_class($extension);
                        $extension->id = $extensionClass;

                        $priority = $this->extensionStore[$extension->id];

                        if ( isset ($priority))
                        {
                            $extension->originalPriority = $extension->priority;
                            $extension->priority = $priority;
                        }
                        $this->extensionManager->register($extension->extendable, $extension);
                    }
                }
            }
        }
    }

    private function trimrslash($path)
    {
        while (strrpos($path, "/") == strlen($path)-1)
        {
            $path = substr($path, 0, strlen($path)-1);
        }
        return $path;
    }

    private function importFeatureStore()
    {
        if ($this->runtimePath)
        {
            $featureStoreAbsFile = $this->runtimePath.DIRECTORY_SEPARATOR.$this->featureStoreFile;
            if (file_exists($featureStoreAbsFile))
            {
                $featureStoreFileContent = file_get_contents($featureStoreAbsFile);
                if ($featureStoreFileContent)
                {
                    $unserialized = unserialize($featureStoreFileContent);
                    if ($unserialized && is_array($unserialized))
                    {
                        $this->featureStore = $unserialized;
                    }
                }
            }
        }
        foreach ($this->preservedFeatures as $featureId)
        {
            $this->featureStore[$featureId] = 'ENABLED';
        }
    }

    private function exportFeatureStore()
    {
        foreach ($this->preservedFeatures as $featureId)
        {
            $this->featureStore[$featureId] = 'ENABLED';
        }
        if ($this->runtimePath)
        {
            $featureStoreAbsFile = $this->runtimePath.DIRECTORY_SEPARATOR.$this->featureStoreFile;
            $featureStoreFileContent = serialize($this->featureStore);
            file_put_contents($featureStoreAbsFile, $featureStoreFileContent);
        }
    }

    private function importExtensionStore()
    {
        if ($this->runtimePath)
        {
            $extensionStoreAbsFile = $this->runtimePath.DIRECTORY_SEPARATOR.$this->extensionStoreFile;
            if (file_exists($extensionStoreAbsFile))
            {
                $extensionStoreFileContent = file_get_contents($extensionStoreAbsFile);
                if ($extensionStoreFileContent)
                {
                    $unserialized = unserialize($extensionStoreFileContent);
                    if ($unserialized && is_array($unserialized))
                    {
                        $this->extensionStore = $unserialized;
                    }
                }
            }
        }
    }

    private function exportExtensionStore()
    {
        if ($this->runtimePath)
        {
            $extensionStoreAbsFile = $this->runtimePath.DIRECTORY_SEPARATOR.$this->extensionStoreFile;
            $extensionStoreFileContent = serialize($this->extensionStore);
            file_put_contents($extensionStoreAbsFile, $extensionStoreFileContent);
        }
    }

    public function apply($id, & $context)
    {
        $this->extensionManager->apply($id, & $context);
    }

    public function getFeatures()
    {
        return $this->features;
    }

    public function getExtendablesByFeature($id)
    {
        $extentables = array ();
        foreach ($this->features[$id]->extendables as $extendable)
        {
            $extentables[$extendable->id] = $extendable;
        }
        return $extentables;
    }

    public function getExtensionsByExtendable($id)
    {
        $allExtensions = $this->extensionManager->getExtensions();
        $prioredExtensions = $allExtensions[$id];
        if (is_array($prioredExtensions))
        {
            ksort($prioredExtensions);
        }
        $extensions = array ();
        if ($prioredExtensions)
        {
            foreach ($prioredExtensions as $priority)
            {
                $extensions = array_merge($extensions, $priority);
            }
        }
        return $extensions;
    }

    public function updateFeatureStatus($id, $status)
    {
        if (in_array($status, Array('ENABLED', 'SUSPENDED', 'DISABLED')))
        {
            $feature = $this->features[$id];
			$previousStatus = $feature->status;
            $feature->status = $status;
            $this->featureStore[$id] = $status;
            if ($status == "DISABLED")
            {
                unset ($this->featureStore[$id]);
            }
            $this->exportFeatureStore();
            if ($status == "ENABLED" && $previousStatus == "DISABLED")
            {
                $feature->init();
            }
            elseif ($status == "DISABLED")
            {
                $feature->destroy();
            }

            //remove extension store assosiated with the disabled feature
            if ($status == "DISABLED")
            {
                $extensions = $feature->extensions;
                foreach ($extensions as $extension)
                {
                    unset ($this->extensionStore[$extension->id]);
                }
                $this->exportExtensionStore();
            }
        }
    }

    public function updateExtensionPriority($id, $extendableId, $direction)
    {
        if (in_array($direction, Array('up', 'down')))
        {
            $extensions = $this->getExtensionsByExtendable($extendableId);
            $ExtensionArray = Array();
            $i = 0;
            foreach ($extensions as $extensionId=>$extension)
            {
                $ExtensionArray[$i] = $extension;
                if ($extensionId == $id)
                {
                    $position = $i;
                }
                $i++;
            }
            if ($direction == 'up')
            {
                $temp = $ExtensionArray[$position-1];
                $ExtensionArray[$position-1] = $ExtensionArray[$position];
                $ExtensionArray[$position] = $temp;
            }
            elseif ($direction == 'down')
            {
                $temp = $ExtensionArray[$position+1];
                $ExtensionArray[$position+1] = $ExtensionArray[$position];
                $ExtensionArray[$position] = $temp;
            }
            foreach ($ExtensionArray as $i=>$extension)
            {
                $this->extensionStore[$extension->id] = $i;
            }
            $this->exportExtensionStore();
        }
    }

    public function resetExtendablePriorities($id)
    {
        $extensions = $this->getExtensionsByExtendable($id);
        foreach ($extensions as $extension)
        {
            unset ($this->extensionStore[$extension->id]);
        }
        $this->exportExtensionStore();
    }
}

?>
